import 'dart:async';
import 'dart:convert';
import 'dart:io';

// ignore: depend_on_referenced_packages
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;

import '../config/url_config.config.dart';
import '../constants/http_method.constant.dart';
import '../models/error_details.model.dart';
import './http_response_code_manager.service.dart';
import './shared_util.service.dart';

typedef UploadProgressCallback = void Function(int progress);
typedef SuccessCallback = void Function(Map<String, dynamic> response);
typedef FailureCallback = void Function(Map<String, dynamic> error);

/// Http request service which interacts with backend service.
///
/// This service handles any common task. Such like token authentication, http error handling.
class HttpRequestService {
  static const String _hostBaseUrl = UrlConfig.apiHostBaseUrl;
  static const String _httpBaseUrl = UrlConfig.apiHttpBaseUrl;

  static const String _httpResponseSuccessKeyName = 'success';
  static const String _httpResponsePayloadKeyName = 'payload';

  // Defines when http request should raise timeout exception.
  // The short timeout duration works in release mode only.
  static const int _httpRequestTimeoutInSeconds = kReleaseMode ? 60 : 9999;

  /// Combines the provided [httpBaseUrl] and relative [url].
  static String combineUrl(String httpBaseUrl, String url) {
    if (!SharedUtil.checkNullOrEmpty(httpBaseUrl)) {
      // Converts the input url to lower case just in case.
      // url = url.toLowerCase();

      // Checks if the input url already has a leading slash.
      if (!url.startsWith('/')) {
        // Appends the leading slash if needed.
        url = '/$url';
      }

      // Checks if the input url already starts with the base url.
      if (url.startsWith(httpBaseUrl)) {
        // If already contains the base url.
        return url;
      } else {
        // Appends the base url if needed.
        return httpBaseUrl + url;
      }
    } else {
      return url;
    }
  }

  /// Combines the relative [url] with default base url.
  static String combineUrlWithDefault(String url) {
    return combineUrl(_httpBaseUrl, url);
  }

  /// Makes the http get request.
  ///
  /// Example:
  /// ```dart
  /// get(
  ///      '/product/list',
  ///      params: {
  ///        'pageSize': 20,
  ///        'pageIndex': 2,
  ///      },
  ///    )
  /// ```
  static Future<HttpResponseResult> get(String url,
      {Map<String, dynamic>? params}) async {
    return _makeRequest(
      HttpMethod.get,
      _hostBaseUrl,
      _httpBaseUrl,
      url,
      params,
    );
  }

  /// Makes the http post request.
  ///
  /// Example:
  /// post(
  ///      '/order/save',
  ///      body: {
  ///        'name': 'New Order',
  ///        'price': 100,
  ///      },
  ///    )
  static Future<HttpResponseResult> post(String url,
      {Map<String, dynamic>? body}) async {
    return _makeRequest(
      HttpMethod.post,
      _hostBaseUrl,
      _httpBaseUrl,
      url,
      body,
    );
  }

  /// Prepares the uri for http request, especially, for http get request.
  /// Because there is no way you can pass in the params to http.get method directly.
  ///
  /// The [hostBaseUrl] likes 'http://192.168.1.159:8080'.
  /// The [httpBaseUrl] likes '/we-shop-api'.
  /// The [url] likes '/product/list'.
  ///
  /// The full example code:
  /// ```dart
  ///   _prepareUri(
  ///     'http://192.168.1.159:8080',
  ///     '/we-shop-api',
  ///     '/product/list',
  ///     {
  ///       'id': 10,
  ///       'name': 'Darvis',
  ///     },
  ///   )
  /// ```
  static Uri? _prepareUri(
    String hostBaseUrl,
    String httpBaseUrl,
    String url, {
    Map<String, dynamic>? params = const {'': ''},
  }) {
    const httpPrefix = 'http://';
    const httpsPrefix = 'https://';

    // IMPORTANT!!!
    // We are doing the parsing because the input params type is [Map<String, dynamic>].
    // But the Uri constructor only takes [Map<String, String>].
    // If you use the mismatched type the code will fail without any exception.
    Map<String, String>? parsedParams =
        params?.map((key, value) => MapEntry(key, value.toString()));

    // The host url likes 'http://192.168.1.159:8080'.
    hostBaseUrl = hostBaseUrl.toLowerCase();

    // The [Uri] will be used for http get method to pass the params.
    Uri? uri;

    // The [Uri] constructor treats http and https differently.
    if (hostBaseUrl.startsWith(httpPrefix)) {
      uri = Uri.http(
        hostBaseUrl.replaceFirst(httpPrefix, ''),
        combineUrl(httpBaseUrl, url),
        parsedParams,
      );
    } else if (hostBaseUrl.startsWith(httpsPrefix)) {
      uri = Uri.https(
        hostBaseUrl.replaceFirst(httpsPrefix, ''),
        combineUrl(httpBaseUrl, url),
        parsedParams,
      );
    }

    return uri;
  }

  /// Creates the customized http request header.
  static Future<Map<String, String>> _createRequestHeader() async {
    final header = <String, String>{};
    header[HttpHeaders.contentTypeHeader] = 'application/json';

    return header;
  }

  /// Makes the http request based on [httpMethod].
  /// It will do the token check and try to refresh it if it can.
  ///
  /// DO NOT DELETE THE COMMENTS IN THE METHOD. THESE ARE FOR TESTING.
  ///
  /// Previous version uses sleep function to wait and retry when token is refreshing. This can be migrated to pure JavaScript code.
  /// Current version uses Completer class to notify when the refreshing token is done. This is similar with Angular Observable class.
  static Future<HttpResponseResult> _makeRequest(
    String httpMethod,
    String hostBaseUrl,
    String httpBaseUrl,
    String url,
    Map<String, dynamic>? data,
  ) async {
    Uri? uri;
    late http.Response response;

    // For debug purpose only.
    // print(
    //     'Ready to request at ${SharedUtil.formatDate(SharedUtil.getCurrentDateTime(), 'HH:mm:ss.SSS')}');

    try {
      final headers = await _createRequestHeader();

      if (httpMethod == HttpMethod.get) {
        // Gets the [Uri] to call the [http.get] for passing the params.
        uri = _prepareUri(hostBaseUrl, httpBaseUrl, url, params: data);

        // Gets the http get request response.
        response = await http
            .get(uri!, headers: headers)
            .timeout(const Duration(seconds: _httpRequestTimeoutInSeconds));
      } else if (httpMethod == HttpMethod.post) {
        // Gets the [Uri] to call the [http.get] for passing the params.
        uri = _prepareUri(hostBaseUrl, httpBaseUrl, url);

        // Gets the http post request response.
        // IMPORTANT!!!
        // We are doing toString because the input body type is [Map<String, dynamic>].
        // But the [http.post] needs [String] format to be attached in the http request body.
        response = await http
            .post(uri!, headers: headers, body: json.encode(data))
            .timeout(const Duration(seconds: _httpRequestTimeoutInSeconds));
      }

      return _checkHttpResponse(response);
    } on TimeoutException {
      _showMessage('网络连接超时，服务可能无响应或您的网络无法正常访问，请稍后重试。');
      return HttpResponseResult(false);
    } on SocketException {
      _showMessage('网络连接失败，服务可能无响应或您的网络无法正常访问，请稍后重试。');
      return HttpResponseResult(false);
    } catch (e) {
      _showMessage('服务数据处理发生未知异常，请稍后重试。');
      return HttpResponseResult(false);
    }
  }

  /// Checks on [response] to see if it is successful or not.
  ///
  /// IMPORTANT!!!
  /// If this method is not async then throwing error from here will not be caught by [runZonedGuarded] in main.dart.
  ///
  /// If it is successful then we will convert it to [Map<String, dynamic>] format.
  /// If the status code is [HttpStatus.unauthorized] we will try to refresh the access token if we can.
  static Future<HttpResponseResult> _checkHttpResponse(
      http.Response response) async {
    HttpResponseResult? httpResponseResult;

    // Checks if the response is successful or not.
    switch (response.statusCode) {
      case HttpStatus.ok:
      case HttpStatus.noContent:
        httpResponseResult = _getResponseResult(response.body);
        break;
      case HttpStatus.unauthorized:
        _handleUnauthorizedError(response);
        break;
      case HttpStatus.forbidden:
        _showMessage('抱歉，您被禁止该访问。');
        break;
      case HttpStatus.notFound:
        _showMessage('当前服务不可访问');
        break;
      default:
        _handleGeneralError(response);
        break;
    }

    // If the [httpResponseResult] means error happens.
    httpResponseResult ??= HttpResponseResult(false);

    return httpResponseResult;
  }

  /// Gets the result from the [response] and converts it to [Map<String, dynamic>] format.
  static HttpResponseResult _getResponseResult(String response) {
    final httpResponseResult = HttpResponseResult(true);
    Map<String, dynamic> res;

    // Checks if the response content is empty.
    if (!SharedUtil.checkNullOrEmpty(response)) {
      // Parses the string and returns the resulting Json object.
      final data = json.decode(response);

      // Parses Json object to Map<String, dynamic>.
      res = Map<String, dynamic>.from(data);

      httpResponseResult.success = res[_httpResponseSuccessKeyName];
      httpResponseResult.payload = res[_httpResponsePayloadKeyName];
    }

    return httpResponseResult;
  }

  /// By now the spring security rest returns [HttpStatus.unauthorized] for different reasons.
  /// We need to check into the header to see what exactly happened.
  /// Handles the error of the [HttpStatus.unauthorized].
  static void _handleUnauthorizedError(http.Response response) {
    final errorDetails =
        _getBodyErrorDetails(response) ?? _getHeaderErrorDetails(response)!;

    if (!SharedUtil.checkNullOrEmpty(errorDetails.message)) {
      // Displays message to user.
      // Actually we did not display message like 'user not found' or 'password is incorrect'.
      // This is for void brute force attack. If the attacker does NOT know the login id or password is correct that would make the attach more difficult.
      _showMessage(errorDetails.message!);
    } else {
      _showMessage('登录已过期、没有访问权限或角色被修改，请重新登录。');
    }
  }

  /// By now the spring security rest returns [HttpStatus.unauthorized] for different reasons.
  /// We need to check into the header to see what exactly happened.
  /// Handles the error of the [HttpStatus.unauthorized].
  static void _handleGeneralError(http.Response response) {
    ErrorDetails? errorDetails;

    try {
      errorDetails =
          _getBodyErrorDetails(response) ?? _getHeaderErrorDetails(response)!;
    } catch (_) {
      // Just make sure error will not stop processing.
    }

    var errorMessage = '服务处理发生问题';

    if (!SharedUtil.checkNullOrEmpty(errorDetails?.message)) {
      errorMessage += '：${errorDetails!.message}。';
    }

    _showMessage(errorMessage);
  }

  /// Gets the response error from body.
  static ErrorDetails? _getBodyErrorDetails(http.Response response) {
    ErrorDetails? errorDetails;

    if (!SharedUtil.checkNullOrEmpty(response.body)) {
      // Decodes the response body from string to map.
      final bodyJson = json.decode(response.body);

      // Parses Json object to Map<String, dynamic>.
      final bodyData = Map<String, dynamic>.from(bodyJson);

      // Gets the error message from the body data.
      errorDetails = _getErrorDetails('message', bodyData);
    }

    return errorDetails;
  }

  /// Gets response error from header.
  static ErrorDetails? _getHeaderErrorDetails(http.Response response) {
    return _getErrorDetails('Error-Code', response.headers);
  }

  /// Gets the error message from [map] which might be the response body or header.
  /// The [key] which used to find in the map to see if it contains error code.
  static ErrorDetails? _getErrorDetails(String key, Map<String, dynamic> map) {
    String? errorMessage;
    String? errorCode;

    // Tries to get the error key from http header if the header key contains [key].
    // Uses orElse parameter to slient 'No Element' error if no match found.
    final errorKey =
        map.keys.firstWhereOrNull((p) => p.toLowerCase().contains(key));

    // If error code exists.
    if (!SharedUtil.checkNullOrEmpty(errorKey)) {
      // Gets the code by the key.
      errorCode = map[errorKey!];

      // Gets the message by code.
      errorMessage = HttpResponseCodeManager.getCodeMessage(errorCode!);

      return ErrorDetails(errorCode, errorMessage);
    }

    return null;
  }

  /// Shows message to user if anything goes wrong.
  static void _showMessage(String msg) {}
}

/// Used as the final http response result.
/// The [success] property used to indicate if the request is successful or not.
class HttpResponseResult {
  HttpResponseResult(this.success);

  bool success;
  dynamic payload;
}
